#include <Time.h>
#include <EEPROM.h>

// Define the two input pins, and the state which means they are being pressed.
#define MODE		19
#define MODE_DOWN	LOW
#define SET		0
#define SET_DOWN	LOW

/*
 * Sets of pins used for the LED displays.
 * These are LSB first, with a trailing 0 to indicate the last LED in the set.
 * Note that this means we can't drive an LED with pin 0. Use it for a switch.
 */
static int dow_pins[] = {4, 3, 2, 0} ;
static int ampm_pins[] = {5, 6, 0} ;
static int hour_pins[] = {11, 10, 9, 8, 7, 0} ;
static int minute_pins[] = {17, 16, 15, 14, 13, 12, 0} ;
static int second_pins[] = {18, 0} ;

/*
 * We use a single blinking LED for seconds. By making it an RGB LED
 * and setting the ampm pins to the other two color pins in it, we can
 * change the color of the blink to indicate 24 hour mode, am, or pm.
 * Define AMPMRGB if that is the case.
 */
//#define AMPMRGB

// Define the delays on button actions
#define DEBOUNCE	50	// debounce time
#define AUTO_DELAY	200	// Automatic counter
#define LONG_DELAY	300	// Delay befoer starting automatic counter

// We want to use the internal pullup resistors if we can.
#define INPUT_TYPE(down) (down == LOW ? INPUT_PULLUP : INPUT)

/*
 * Now the structure used to drive the display.
 * Triples consisting of the pin arrays from above, a function to extract
 * the displayed value from a time_t, and a function to increment the
 * displayed value in a time_t.
 * Terminated by a pair of NULL pointers.
 */
typedef struct {
  int *pins ;			// The pin array for this display
  int (*fetch)(time_t) ;	// Function to fetch value from time_t
  void (*adjuster)(time_t) ;	// Adjust this value
} Display ;

// Forwards for the table below.
static void adjust_dow(time_t) ;
static void adjust_ampm(time_t) ;
static void adjust_hour(time_t) ;
static void adjust_minute(time_t) ;

// Value fetchers not from the time library.
static int hour_value(time_t) ;
static int ampm_value(time_t) ;

/*
 * And now the data structure that drives it all. The final NULLs serve
 * as an end of data marker, and the "normal mode" indicator.
 * The blinker_pins and "normal" row are both set to BLINKER, 
 */
Display displays[] = {{dow_pins, weekday, adjust_dow} ,
		      {ampm_pins, ampm_value, adjust_ampm} ,
		      {hour_pins, hour_value, adjust_hour} ,
		      {minute_pins, minute, adjust_minute} ,
		      {second_pins, second, NULL} ,
		      {NULL, NULL, NULL} ,	// End of list marker.
} ;
#define MODE_MOD	(sizeof(displays) / sizeof(Display) - 1)
// Index to entry used for running mode
#define NORMAL		(MODE_MOD - 1)


// Operating mode globals (oh well).
static int mode = NORMAL ;
static int ampm = false ;

// EEPROM location we store am/pm setting.
#define AMPM	0

void
setup() {
  // Input pins set for input. 
  pinMode(MODE, INPUT_TYPE(MODE_DOWN)) ;
  pinMode(SET, INPUT_TYPE(SET_DOWN)) ;

  // And now all those output pins!
  for (Display *dsp = displays; dsp->pins; dsp++) {
    for (int *pin = dsp->pins; *pin; pin++) {
      pinMode(*pin, OUTPUT) ;
    }
  }

  ampm = EEPROM.read(AMPM) ;
  if (ampm == 255) {
    ampm = false ;
  }
}


// Forwards used in loop.
static void blink(int) ;
static void display_time(time_t) ;
static void display_value(int *, int) ;

void
loop() {
  int set_value, mode_value ;
  static int old_mode_value = !MODE_DOWN ;
  static int old_set_value = !SET_DOWN ;

  // Blink the LEDs for the current mode when we change modes.
  mode_value = digitalRead(MODE) ;
  if (mode_value == MODE_DOWN && old_mode_value != MODE_DOWN) {
    mode = (mode + 1) % MODE_MOD ;
    delay(DEBOUNCE) ;
    blink(mode) ;	// Now set the RGB blinker for our mode
  }
  old_mode_value = mode_value ;

  // If set is down, adjust the items for mode
  set_value = digitalRead(SET) ;
  if (set_value == SET_DOWN && displays[mode].adjuster != NULL) {
    /*
     * Delay after the button press so you can increment by tapping, then
     * a shorter delay to autoincrement at reasonable speed.
     */
    delay(old_set_value == SET_DOWN ? AUTO_DELAY : LONG_DELAY) ;
    displays[mode].adjuster(now()) ;
  }
  old_set_value = set_value ;

  display_time(now()) ;
  delay(1) ;
}


// If we can adjust something, blink it to indicate that we can.
static void
blink(int m) {
  Display *dsp = &displays[m] ;
  
  if (dsp->adjuster) {
    for (int i = 0; i < 4; i++) {
      display_value(dsp->pins, 0) ;
      delay(125) ;
      display_value(dsp->pins, ~0) ;
      delay(125) ;
    }
  display_value(dsp->pins, dsp->fetch(now())) ;
  }
}


// Hour & ampm values depends on whether or not we have a 12 or 24 hour clock.
static int
hour_value(time_t t) {
  return ampm ? hourFormat12(t) : hour(t) ;
}

#ifdef AMPMRGB		// Invert bits for RGB LED.
#define FIXAMPM ~
#else
#define FIXAMPM
#endif
static int
ampm_value(time_t t) {
  return FIXAMPM(ampm ? isPM() + 1 : 0) ;
}  

// Send the current time to the available displays.
static void
display_time(time_t t) {
  for (Display *dsp = displays; dsp->pins; dsp++) {
    display_value(dsp->pins, dsp->fetch(t)) ;
  }
}

// Display the bits in a value, one bit per led
static void
display_value(int *array, int value) { 
  while (*array) {
    digitalWrite(*array++, value & 1) ;
    value >>= 1 ;
  }
}

/*
 * Routines for adjusting settings.
 * Each is called when the SET button is pressed to advance the selected
 * objects to the next state.
 */
static void
adjust_dow(time_t t) {
  setTime(hour(t), minute(t), second(t), (day(t) % 7) + 1, month(t), year(t)) ;
}

// adjusting ampm adjust our display mode, not the time, and is saved to EEPROM
static void
adjust_ampm(time_t) {
  ampm = !ampm ;
  EEPROM.write(AMPM, ampm) ;
}

static void
adjust_hour(time_t t) {
  setTime((hour(t) + 1) % 24, minute(t), second(t), day(t), month(t), year(t)) ;
}

/*
 * Minute resets seconds. Set the minute when your reference time changes
 * to get seconds mostly right.
 */
static void
adjust_minute(time_t t) {
  setTime(hour(t), (minute(t) + 1) % 60, 0, day(t), month(t), year(t)) ;
}
